package com.lu.controller;
import java.math.BigDecimal;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONArray;
import com.lu.model.ArticleEs;
import com.lu.model.EsSuggestModel;
import com.lu.service.ArticleEsRepository;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.WeightBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.SortBy;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.FuzzyOptions;
import org.elasticsearch.search.suggest.completion.context.CategoryQueryContext;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.completion.Completion;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @program ElasticsearchLearn
 * @description: 基于sKnowledge产出的文章，通过es技术，做一些有意思的事情。包括但不仅限于：索引构建、分词器、实时搜索、纠正检索词
 *  - 演示步骤
 *      - 引入项目相对目录 software es的分词器配置
 *          https://github.com/medcl/elasticsearch-analysis-ik
 *          https://github.com/medcl/elasticsearch-analysis-pinyin
 *      - 构建索引模版：参考执行 resources 目录下 sk_article_init_rule.json文件
 *      - 调用接口esDataInit初始化数据
 *      - 调用接口调测
 *  - 参考博文：
 *      https://www.elastic.co/guide/en/elasticsearch/reference/6.8/analysis.html
 *      https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.operations.queries
 *      http://t.zoukankan.com/bubu99-p-13593847.html
 * @author: zhanglu
 * @create: 2022-05-06 18:10:00
 */
@RequestMapping("/article")
@RestController
public class ArticleController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private ArticleEsRepository articleEsRepository;

    /**
     * 初始化数据
     * @return
     * @throws FileNotFoundException
     */
    @GetMapping("/esDataInit")
    private String dataInit() throws FileNotFoundException {
        try {
            articleEsRepository.deleteAll();
        } catch (Exception e){
            if(!e.getMessage().contains("index_not_found_exception")){
                throw e;
            }
        }
        File file = ResourceUtils.getFile("classpath:sk_article_data.json");
        String json = FileUtil.readUtf8String(file);
        List<ArticleEs> articleEsList = JSONArray.parseArray(json, ArticleEs.class);
        for (ArticleEs articleEs : articleEsList) {
            if(articleEs.getUserId() == null){
                articleEs.setUserId(1L);
            }
            articleEs.setHeaderPname(this.articleHeaderConvert(articleEs.getHeaderPid()));
            articleEs.setHeaderName(this.articleHeaderConvert(articleEs.getHeaderId()));

            Completion titleCompletion = new Completion(new String[]{articleEs.getTitle()});
            Map<String, List<String>> titleContext = new HashMap<>();
            titleContext.put("channel", Collections.singletonList("sKnowledge"));
            titleContext.put("author", Collections.singletonList(articleEs.getAuthor()));
            titleContext.put("headerPname", Collections.singletonList(articleEs.getHeaderPname()));
            titleContext.put("headerName", Collections.singletonList(articleEs.getHeaderName()));
            titleCompletion.setContexts(titleContext);
            articleEs.setTitleSuggest(titleCompletion);

            Completion descriptCompletion = new Completion(new String[]{articleEs.getTitle()});
            Map<String, List<String>> descriptContext = new HashMap<>();
            descriptContext.put("channel", Collections.singletonList("sKnowledge"));
            descriptContext.put("author", Collections.singletonList(articleEs.getAuthor()));
            descriptContext.put("headerPname", Collections.singletonList(articleEs.getHeaderPname()));
            descriptContext.put("headerName", Collections.singletonList(articleEs.getHeaderName()));
            descriptCompletion.setContexts(descriptContext);
            articleEs.setDescriptSuggest(descriptCompletion);

            Completion contentCompletion = new Completion(new String[]{articleEs.getTitle()});
            Map<String, List<String>> contentContext = new HashMap<>();
            contentContext.put("channel", Collections.singletonList("sKnowledge"));
            contentContext.put("author", Collections.singletonList(articleEs.getAuthor()));
            contentContext.put("headerPname", Collections.singletonList(articleEs.getHeaderPname()));
            contentContext.put("headerName", Collections.singletonList(articleEs.getHeaderName()));
            contentCompletion.setContexts(contentContext);
            articleEs.setContentSuggest(contentCompletion);

            articleEsRepository.save(articleEs);
        }
        return "ok";
    }

    @GetMapping("/syncDataToEs")
    public String syncDataToEs(){
        //http请求数据
        return "1";
    }

    /**
     * 实时搜索：
     *   文档参考：https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-suggesters-completion.html
     *   本地测试：http://127.0.0.1:8001/article/search?suggestField=titleSuggest&keyWord=使用
     * @return
     */
    @GetMapping("/search")
    public Object search(String keyWord, String suggestField) throws IOException {
        // 创建suggestBuilder并将completionBuilder添加进去
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder
                .addSuggestion("simple_suggest", SuggestBuilders.completionSuggestion(suggestField)
                        .prefix(keyWord).skipDuplicates(true).contexts(Collections.singletonMap("channel",
                                Collections.singletonList(CategoryQueryContext.builder().setCategory("sKnowledge").setBoost(3).build()))))
                .addSuggestion("fuzzy_simple_suggest", SuggestBuilders.completionSuggestion(suggestField)
                        .prefix(keyWord, FuzzyOptions.builder().build()).skipDuplicates(true).contexts(Collections.singletonMap("channel",
                                Collections.singletonList(CategoryQueryContext.builder().setCategory("sKnowledge").setBoost(2).build()))))
                .addSuggestion("pinyin_suggest", SuggestBuilders.completionSuggestion(suggestField + ".pinyin_suggest")
                        .prefix(keyWord).skipDuplicates(true).contexts(Collections.singletonMap("channel",
                                Collections.singletonList(CategoryQueryContext.builder().setCategory("sKnowledge").setBoost(4).build()))))
                .addSuggestion("full_pinyin_suggest", SuggestBuilders.completionSuggestion(suggestField + ".full_pinyin_suggest")
                        .prefix(keyWord).skipDuplicates(true).contexts(Collections.singletonMap("channel",
                                Collections.singletonList(CategoryQueryContext.builder().setCategory("sKnowledge").setBoost(5).build()))))
                .addSuggestion("first_pinyin_suggest", SuggestBuilders.completionSuggestion(suggestField + ".first_pinyin_suggest")
                        .prefix(keyWord).skipDuplicates(true).contexts(Collections.singletonMap("channel",
                                Collections.singletonList(CategoryQueryContext.builder().setCategory("sKnowledge").setBoost(1).build()))))
        ;

        // 执行请求
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.suggest(suggestBuilder);
        sourceBuilder.fetchSource(false);
        sourceBuilder.explain(false);
        SearchRequest searchRequest = new SearchRequest("sk_article_v1");
        searchRequest.source(sourceBuilder);
        SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        // 取出结果
        List<EsSuggestModel> modelList = new LinkedList<>();
        Suggest suggest = search.getSuggest();
        for (String suggestName : Arrays.asList("simple_suggest", "fuzzy_simple_suggest", "pinyin_suggest", "full_pinyin_suggest", "first_pinyin_suggest")) {
            Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> simpleSuggest = suggest.getSuggestion(suggestName);
            for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> entry : simpleSuggest.getEntries()) {
                List<? extends Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                for (Suggest.Suggestion.Entry.Option option : options) {
                    EsSuggestModel model = new EsSuggestModel();
                    model.setId(((CompletionSuggestion.Entry.Option) option).getHit().getId());
                    model.setScore(new BigDecimal(option.getScore()));
                    model.setText(option.getText().toString());
                    model.setSourceText(keyWord);
                    model.setSuggestName(simpleSuggest.getName());
                    model.setSuggestField(suggestField);
                    modelList.add(model);
                }
            }
        }
        //优先返回得分高的，建议类型分组
        return modelList.stream().sorted(Comparator.comparing(EsSuggestModel::getScore).reversed()).collect(Collectors.groupingBy(EsSuggestModel::getSuggestName));
    }

    /**
     * 检索词纠正：
     *  文档参考：https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-suggesters-phrase.html
     *  本地测试：http://127.0.0.1:8001/article/correct?suggestField=title&keyWord=mmbatis%20%E6%95%B4%E5%90%88%20neo4j
     * @return
     */
    @GetMapping("/correct")
    public Object correct(String keyWord, String suggestField) throws IOException {
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder.setGlobalText(keyWord);
        suggestBuilder.addSuggestion("term_suggest", SuggestBuilders.termSuggestion(suggestField).suggestMode(TermSuggestionBuilder.SuggestMode.ALWAYS).sort(SortBy.FREQUENCY).analyzer("ik_smart"));
        suggestBuilder.addSuggestion("phrase_suggest", SuggestBuilders.phraseSuggestion(suggestField).highlight("<em>", "</em>").analyzer("ik_smart"));
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.suggest(suggestBuilder);
        sourceBuilder.fetchSource(false);
        sourceBuilder.explain(false);
        SearchRequest searchRequest = new SearchRequest("sk_article_v1");
        searchRequest.source(sourceBuilder);
        SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        // 取出结果
        List<EsSuggestModel> modelList = new LinkedList<>();
        Suggest suggest = search.getSuggest();
        for (String suggestName : Arrays.asList("term_suggest", "phrase_suggest")) {
            Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> simpleSuggest = suggest.getSuggestion(suggestName);
            for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> entry : simpleSuggest.getEntries()) {
                List<? extends Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                if(ObjectUtil.isEmpty(entry.getOptions())){
                    EsSuggestModel model = new EsSuggestModel();
                    model.setScore(BigDecimal.ZERO);
                    model.setText(entry.getText().toString());
                    model.setSourceText(keyWord);
                    model.setSuggestName(simpleSuggest.getName());
                    model.setSuggestField(suggestField);
                    modelList.add(model);
                    continue;
                }
                for (Suggest.Suggestion.Entry.Option option : options) {
                    EsSuggestModel model = new EsSuggestModel();
                    model.setScore(new BigDecimal(option.getScore()));
                    model.setText(option.getText().toString());
                    model.setSourceText(keyWord);
                    model.setSuggestName(simpleSuggest.getName());
                    model.setSuggestField(suggestField);
                    modelList.add(model);
                }
            }
        }
        //优先返回得分高的，建议类型分组
        return modelList.stream().sorted(Comparator.comparing(EsSuggestModel::getScore).reversed()).collect(Collectors.groupingBy(EsSuggestModel::getSuggestName));
    }

    /**
     * 查询结果多维度排序
     *  文档参考：https://www.elastic.co/guide/en/elasticsearch/reference/6.8/query-dsl-function-score-query.html
     *  本地测试：http://127.0.0.1:8001/article/queryPage?field=title&page=0&size=5&keyWord=%E4%BD%BF%E7%94%A8
     * @param keyWord
     * @param field
     * @param page
     * @param size
     * @return
     * @throws IOException
     */
    @GetMapping("/queryPage")
    public Object queryPage(String keyWord, String field, Integer page, Integer size) throws IOException {
        String inlineScript = "def res = _score; " +
                "if(doc['views'].size() != 0){" +
                "res += doc['views'].value * 0.1;} " +
                "if(doc['thumbsUp'].size() != 0){" +
                "res += doc['thumbsUp'].value * 0.4;}" +
                " return res;";
        Script script = new Script(ScriptType.INLINE, "painless", inlineScript, Collections.emptyMap());
        FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(new MatchQueryBuilder(field, keyWord),
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(new TermQueryBuilder(field + ".keyword", keyWord), new WeightBuilder().setWeight(3)),
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(new TermQueryBuilder(field + ".keyword", "oracle优秀的sql"), new WeightBuilder().setWeight(30)),
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder(new ScriptScoreFunctionBuilder(script))
                });
        functionScoreQueryBuilder.scoreMode(FunctionScoreQuery.ScoreMode.SUM);
//        SearchQuery searchQuery = new NativeSearchQueryBuilder()
//                .withQuery(functionScoreQueryBuilder)
//                .withPageable(PageRequest.of(page, size))
//                .withSort(SortBuilders.fieldSort("_score").order(SortOrder.DESC))
//                .withHighlightFields()
//                .build();
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(functionScoreQueryBuilder).from(page).size(size).sort("_score");
        SearchRequest searchRequest = new SearchRequest("sk_article_v1");
        searchRequest.source(sourceBuilder);
        SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        return search;
    }

    public String articleHeaderConvert(Long articleHeaderId){
        if(articleHeaderId == 1){
            return "Java";
        }else if(articleHeaderId == 4){
            return "MySql";
        }else if(articleHeaderId == 9){
            return "MyBatis";
        }else if(articleHeaderId == 14){
            return "开发工具";
        }else if(articleHeaderId == 20){
            return "Oracle";
        }else if(articleHeaderId == 23){
            return "其他大类";
        }else {
            return "其他小类";
        }
    }

}
